#!/usr/bin/env ruby

# gbconv -- a command-line gamebook generator
# Copyright (C) 2007, 2008 Jaromir Hradilek

# This program is  free software:  you can redistribute it and/or modify it
# under  the terms  of the  GNU General Public License  as published by the
# Free Software Foundation, version 3 of the License.
# 
# This program  is  distributed  in the hope  that it will  be useful,  but
# WITHOUT  ANY WARRANTY;  without  even the implied  warranty of MERCHANTA-
# BILITY  or  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
# 
# You should have received a copy of the  GNU General Public License  along
# with this program. If not, see <http://www.gnu.org/licenses/>.


# Abstract class:
class GBConv
  # General information about the utility:
  NAME     = File.basename(__FILE__, '.rb')      # Utility name.
  VERSION  = '0.7.2'                             # Utility version.

  # The object constructor:
  def initialize
    @gamePart = false                            # Currently parsed part.
    @title    = ''                               # Game title.
    @author   = ''                               # Game author.
    @year     = ''                               # Game year.
    @sections = []                               # List of sections.
    @links    = []                               # List of links.
  end

  # Parse given input and initiate its conversion:
  def convert(input, output)
    # Read header:
    @title, @author, @year = input.get_header

    # Add warnings if any:
    displayWarning("Missing option `title'")  if @title.empty?
    displayWarning("Missing option `author'") if @author.empty?

    # Write document header and front matter:
    output.print convertHeader
    output.print convertFrontMatter

    # Process the rest of the file:
    while temp = input.get_block
      block, lines = temp
      output.print convertBlock(block, lines)
    end

    # Write back matter and footer:
    output.print convertBackMatter
    output.print convertFooter

    # Check references:
    missing = (@links - @sections).uniq.sort.collect { |value|
              "[#{value}] " }.join
    unref   = (@sections - @links).uniq.sort.collect { |value|
              "[#{value}] " }.join

    # Add warnings if any:
    displayWarning("Missing sections: #{missing}") unless  missing.empty?
    displayWarning("Unreferenced sections: #{unref}") unless unref.empty?
  end

  # Display given warning to the standard error output:
  def displayWarning(text, line=nil)
    text += " at line #{line}" if line
    STDERR.puts "Warning: #{text}"
  end

  # Return formatted block:
  def convertBlock(block, lines)
    # Miscellaneous helper variables:
    emph, strong    = false, false
    linkErr, numErr = false, false

    # Substitute all possibly reserved characters:
    block = convertReservedCharacters(block)

    # Substitute all in-line marks:
    block.gsub!(/'{2,3}|\[\[\d+\]\]|\[[^\]]+\]/m) do |match|
      case match
        when "'''"
          strong = !strong
          strong ? convertStrongOpen : convertStrongClose
        when "''"
          emph   = !emph
          emph   ? convertEmphasisOpen : convertEmphasisClose
        when /\[\[(\d+)\]\]/
          if @gamePart
            @links.push($1.to_i) unless $1 == '1'
            convertNumeralLink($1)
          else
            numErr  = true
            match
          end
        when /\[(\S+:\S+)(\s+\S.*|)\]/m
          unless @gamepart
            convertLink($1, $2)
          else
            linkErr = true
            match
          end
        else match
      end
    end

    # Add warnings if any:
    displayWarning('Link is not allowed',           lines) if linkErr
    displayWarning('Numeral link is not allowed',   lines) if numErr
    displayWarning('Missing closing emphasis mark', lines) if emph
    displayWarning('Missing closing strong mark',   lines) if emph

    # Convert block:
    case block
      when /^==\s*(\d+)\s*==$/
        result = ''
        unless @gamePart
          @gamePart = true
          result    = convertGameMatter
        end
        @sections.push($1.to_i) unless $1 == '1'
        return result += convertNumeralSection($1)
      when /^==\s*([^=]+)==$/
        displayWarning('Chapter is not allowed',    lines) if @gamePart
        return convertChapter($1)
      when /^===\s*([^=]+)===$/
        displayWarning('Section is not allowed',    lines) if @gamePart
        return convertSection($1)
      when /^====\s*([^=]+)====$/
        displayWarning('Subsection is not allowed', lines) if @gamePart
        return convertSubsection($1)
      when /^\*/
        return convertList(block)
      when /^#/
        return convertEnumeration(block)
      when /^;/
        displayWarning('Guidepost is not allowed',  lines) unless @gamePart
        block.each_line do |line|
          if (line =~ /^;\s*(\d+)\s*:\s*\S.*$/) and ($1 != '1')
            @links.push($1.to_i)
          end
        end
        return convertGuidepost(block)
      else
        return convertParagraph(block)
    end
  end

  # Substitute all possibly reserved characters; an abstract method:
  def convertReservedCharacters(block)
    raise NotImplementedError
  end

  # The document header; an abstract method:
  def convertHeader
    raise NotImplementedError
  end

  # The document footer; an abstract method:
  def convertFooter
    raise NotImplementedError
  end

  # The document front matter; an abstract method:
  def convertFrontMatter
    raise NotImplementedError
  end

  # The document back matter;  an abstract method:
  def convertBackMatter
    raise NotImplementedError
  end

  # The document game matter;  an abstract method:
  def convertGameMatter
    raise NotImplementedError
  end

  # The chapter; an abstract method:
  def convertChapter(chapter)
    raise NotImplementedError
  end

  # The section; an abstract method:
  def convertSection(section)
    raise NotImplementedError
  end

  # The subsection; an abstract method:
  def convertSubsection(subsection)
    raise NotImplementedError
  end

  # The numeral section; an abstract method:
  def convertNumeralSection(section)
    raise NotImplementedError
  end

  # The list; an abstract method:
  def convertList(block)
    raise NotImplementedError
  end

  # The enumeration; an abstract method:
  def convertEnumeration(block)
    raise NotImplementedError
  end

  # The guidepost; an abstract method:
  def convertGuidepost(block)
    raise NotImplementedError
  end

  # The paragraph; an abstract method:
  def convertParagraph(block)
    raise NotImplementedError
  end

  # The strong opening mark; an abstract method:
  def convertStrongOpen
    raise NotImplementedError
  end

  # The strong closing mark; an abstract method:
  def convertStrongClose
    raise NotImplementedError
  end

  # The emphasis opening mark; an abstract method:
  def convertEmphasisOpen
    raise NotImplementedError
  end

  # The emphasis closing mark; an abstract method:
  def convertEmphasisClose
    raise NotImplementedError
  end

  # The numeral link mark; an abstract method:
  def convertNumeralLink(link)
    raise NotImplementedError
  end

  # The link mark; an abstract method:
  def convertLink(link, description)
    raise NotImplementedError
  end

  # Access control:
  public  :convert
  private :displayWarning, :convertBlock,  :convertReservedCharacters,
          :convertHeader,  :convertFooter, :convertFrontMatter,
          :convertBackMatter, :convertGameMatter, :convertChapter,
          :convertSection,    :convertSubsection, :convertNumeralSection,
          :convertList, :convertEnumeration, :convertGuidepost,
          :convertParagraph,  :convertStrongOpen, :convertStrongClose,
          :convertEmphasisOpen, :convertEmphasisClose, :convertNumeralLink,
          :convertLink
end


# Conversion to LaTeX, the document class article:
class GBLatex < GBConv
  # The object constructor:
  def initialize(encoding=nil, packages=nil)
    @encoding = encoding                         # Input file encoding.
    @packages = packages                         # List of LaTeX packages.
    super()
  end

  # Substitute all possibly reserved characters:
  def convertReservedCharacters(block)
    block.gsub(/^(#?)(.*)/) do
      $1 + $2.to_latex
    end
  end

  # The document header:
  def convertHeader
    "\\documentclass{article}\n" +
    (@packages ? "\\usepackage{#{@packages}}\n" : '') +
    (@encoding ? "\\usepackage[#{@encoding}]{inputenc}\n" : '') +
    "
     \\newcommand{\\ldquo}{\\textquotedblleft}
     \\newcommand{\\rdquo}{\\textquotedblright}
     \\newcommand{\\numsection}[1]{\\subsection*{\\centering #1}}
     \\newcommand{\\guidepost}[2]{\\par #2\\dotfill\\textbf{#1}}

     \\begin{document}\n\n".gsub(/^[ ]{5}/, '')
  end

  # The document footer:
  def convertFooter
    '\end{document}'
  end

  # The document front matter:
  def convertFrontMatter
    "\\title{#{@title.to_latex}}
     \\author{#{@author.to_latex}}
     \\date{#{@year.to_latex}}
     \\maketitle\n\n".gsub(/^[ ]{5}/, '')
  end

  # The document back matter:
  def convertBackMatter
    ''
  end

  # The document game matter:
  def convertGameMatter
    ''
  end

  # The chapter:
  def convertChapter(chapter)
    "\\section*{#{chapter}}\n\n"
  end

  # The section:
  def convertSection(section)
    "\\subsection*{#{section}}\n\n"
  end

  # The subsection:
  def convertSubsection(subsection)
    "\\subsubsection*{#{subsection}}\n\n"
  end

  # The numeral section:
  def convertNumeralSection(section)
    "\\numsection{#{section}}\n\n"
  end

  # The list:
  def convertList(block)
    block.gsub!(/^\*\s*/, '  \item ')
    "\\begin{itemize} \\itemsep 0pt\n#{block}\n\\end{itemize}\n\n"
  end

  # The enumeration:
  def convertEnumeration(block)
    block.gsub!(/^#\s*/, '  \item ')
    "\\begin{enumerate} \\itemsep 0pt\n#{block}\n\\end{enumerate}\n\n"
  end

  # The guidepost:
  def convertGuidepost(block)
    items = ''
    block.split(/^;/).each do |item|
      next if item.empty?
      section, text = item.split(/:/, 2); section.strip!; text.strip!
      items += "\\guidepost{#{section}}{#{text}}\n"
    end
    "#{items}\n\n"
  end

  # The paragraph:
  def convertParagraph(block)
    "#{block}\n\n"
  end

  # The strong opening mark:
  def convertStrongOpen
    '\textbf{'
  end

  # The strong closing mark:
  def convertStrongClose
    '}'
  end

  # The emphasis opening mark:
  def convertEmphasisOpen
    '\emph{'
  end

  # The emphasis closing mark:
  def convertEmphasisClose
    '}'
  end

  # The numeral link mark:
  def convertNumeralLink(link)
    "\\textbf{#{link}}"
  end

  # The link mark:
  def convertLink(link, description)
    if description.empty?
      "\\emph{#{link.gsub(/^[^:]+:\/{0,2}|\/$/, '')}}"
    else
      "\\emph{#{description}} (#{link})"
    end
  end
end


# Conversion to LaTeX, document class book:
class GBBook < GBLatex
  # The object constructor:
  def initialize(encoding=nil, packages=nil, tocname=nil)
    @tocname  = tocname                          # Table of contents name.
    super()
  end
  # The document header:
  def convertHeader
    "\\documentclass{book}\n" +
    (@packages ? "\\usepackage{#{@packages}}\n" : '') +
    (@encoding ? "\\usepackage[#{@encoding}]{inputenc}\n" : '') +
    (@tocname  ? "\n\\renewcommand{\\contentsname}{#{@tocname}}\n" : '') +
    "
     \\newcommand{\\ldquo}{\\textquotedblleft}
     \\newcommand{\\rdquo}{\\textquotedblright}
     \\newcommand{\\numsection}[1]{\\subsection*{\\centering #1}}
     \\newcommand{\\guidepost}[2]{\\par #2\\dotfill\\textbf{#1}}

     \\begin{document}\n\n".gsub(/^[ ]{5}/, '')
  end

  # The document front matter:
  def convertFrontMatter
    "\\pagestyle{empty}

     \\title{#{@title.to_latex}}
     \\author{#{@author.to_latex}}
     \\date{#{@year.to_latex}}
     \\maketitle

     \\pagestyle{plain}

     \\tableofcontents\n\n".gsub(/^[ ]{5}/, '')
  end

  # The document game matter:
  def convertGameMatter
    "\\cleardoublepage\n" +
    "\\pagestyle{empty}\n\n"
  end

  # The chapter:
  def convertChapter(chapter)
    "\\chapter*{#{chapter}}\n" +
    "\\addcontentsline{toc}{chapter}{#{chapter}}\n\n"
  end

  # The section:
  def convertSection(section)
    "\\section*{#{section}}\n" +
    "\\addcontentsline{toc}{section}{#{section}}\n\n"
  end

  # The subsection:
  def convertSubsection(subsection)
    "\\subsection*{#{subsection}}\n" +
    "\\addcontentsline{toc}{subsection}{#{subsection}}\n\n"
  end
end


# Conversion to troff, without RP macro:
class GBTroff < GBConv
  # Substitute all possibly reserved characters:
  def convertReservedCharacters(block)
    block.to_troff
  end

  # The document header:
  def convertHeader
    ".nr PI 2m\n.nr PD 0v\n"
  end

  # The document footer:
  def convertFooter
    ''
  end

  # The document front matter:
  def convertFrontMatter
    ".TL\n#{@title.to_troff}
     .AU\n#{@author.to_troff}
     .AI\n#{@year.to_troff}\n".gsub(/^[ ]{5}/, '')
  end

  # The document back matter:
  def convertBackMatter
    ''
  end

  # The document game matter:
  def convertGameMatter
    ''
  end

  # The chapter:
  def convertChapter(chapter)
    ".NH 1\n#{chapter}\n"
  end

  # The section:
  def convertSection(section)
    ".NH 2\n#{section}\n"
  end

  # The subsection:
  def convertSubsection(subsection)
    ".NH 3\n#{subsection}\n"
  end

  # The numeral section:
  def convertNumeralSection(section)
    ".SH\n.ce\n#{section}\n"
  end

  # The list:
  def convertList(block)
    ".sp\n.RS\n" + block.gsub(/^\*\s*/, ".IP \\(bu 2\n") + "\n.RE\n.sp\n"
  end

  # The enumeration:
  def convertEnumeration(block)
    ".sp\n.RS\n.nr step 0 1\n" +
    block.gsub(/^#\s*/, ".IP \\n+[step]. 3\n") + "\n.RE\n.sp\n"
  end

  # The guidepost:
  def convertGuidepost(block)
    block.sub(/^;\s*/, '').split(/^;\s*/).collect do |item|
      section, text = item.split(/:/, 2); section.strip!; text.strip!
      ".PP\n" + text.gsub(/\n/, ' ').squeeze(' ') + "...\\fB#{section}\\fP\n"
    end.join
  end

  # The paragraph:
  def convertParagraph(block)
    ".PP\n#{block}\n"
  end

  # The strong opening mark:
  def convertStrongOpen
    '\fB'
  end

  # The strong closing mark:
  def convertStrongClose
    '\fP'
  end

  # The emphasis opening mark:
  def convertEmphasisOpen
    '\fI'
  end

  # The emphasis closing mark:
  def convertEmphasisClose
    '\fP'
  end

  # The numeral link mark:
  def convertNumeralLink(link)
    "\\fB#{link}\\fP"
  end

  # The link mark:
  def convertLink(link, description)
    if description.empty?
      "\\fI#{link.gsub(/^[^:]+:\/{0,2}|\/$/, '')}\\fP"
    else
      "\\fI#{description}\\fP (#{link})"
    end
  end
end

# Conversion to troff, with RP macro:
class GBReport < GBTroff
  # The object constructor:
  def initialize(tocname=nil)
    @tocname = tocname                           # Table of contents name.
    super()
  end

  # The document header:
  def convertHeader
    ".RP no\n.nr PI 2m\n.nr PD 0v\n" +
    (@tocname ? ".ds TOC #{@tocname}\n" : '')
  end

  # The document footer:
  def convertFooter
    ".TC"
  end

  # The document game matter:
  def convertGameMatter
    ".bp\n"
  end

  # The chapter:
  def convertChapter(chapter)
    ".NH 1\n#{chapter}\n.XS\n#{chapter}\n.XE\n"
  end

  # The section:
  def convertSection(section)
    ".NH 2\n#{section}\n.XS\n\t#{section}\n.XE\n"
  end

  # The subsection:
  def convertSubsection(subsection)
    ".NH 3\n#{subsection}\n.XS\n\t\t#{subsection}\n.XE\n"
  end
end

# Conversion to HTML, single page:
class GBHtml < GBConv
  # The object constructor:
  def initialize(encoding=nil)
    @encoding   = encoding || 'UTF-8'            # Input file encoding.
    @chapter    = 0                              # Chapter counter.
    @section    = 0                              # Section counter.
    @subsection = 0                              # Subsection counter.
    super()
  end

  # Substitute all possibly reserved characters:
  def convertReservedCharacters(block)
    block.to_html
  end

  # The document header:
  def convertHeader
    return <<"END_HEADER"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
                      "http://www.w3.org/TR/html4/strict.dtd">

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html;
                                  charset=#{@encoding}">
  <title>#{@title}</title>
  <style type="text/css">
  <!--
    body {
      background-color: #FFFFFF;
      color:            #000000;
    }

    h1 {
      text-align:       center;
    }

    h2.author {
      font-size:        x-large;
      font-weight:      normal;
      text-align:       center;
    }

    h2.year {
      font-size:        medium;
      font-weight:      normal;
      text-align:       center;
    }

    h2.game {
      font-size:        large;
      text-align:       center;
    }

    .guidepost {
      position:         relative;
      padding:          0;
      margin:           0;
    }

    .leader {
      position:         absolute;
      border-bottom:    1px dotted black;
      left:             0px;
      top:              -4px;
      width:            100%;
      height: 100%;
    }

    .left {
      background-color: #FFFFFF;
      position:         relative;
      padding-right:    2px;
    }

    .right {
      background-color: #FFFFFF;
      position:         absolute;
      right:            0;
      bottom:           2px;
      padding-left:     2px;
    }
  -->  
  </style>
  <!--[if IE]>
    <style type="text/css">
      .right { top: 0px; }
    </style>
  <![endif]-->
</head>

<body>
END_HEADER
  end

  # The document footer:
  def convertFooter
    "</body>\n</html>"
  end

  # The document front matter:
  def convertFrontMatter
    "<h1>#{@title.to_html}</h1>\n" +
    "<h2 class=\"author\">#{@author.to_html}</h2>\n" +
    "<h2 class=\"year\">#{@year.to_html}</h2>\n\n"
  end

  # The document back matter:
  def convertBackMatter
    ''
  end

  # The document game matter:
  def convertGameMatter
    ''
  end

  # The chapter:
  def convertChapter(chapter)
    @chapter    += 1
    @section     = @subsection = 0
    number       = "#{@chapter}"
    "<h2><a name=\"sec-#{number}\"></a>#{chapter}</h2>\n\n"
  end

  # The section:
  def convertSection(section)
    @section    += 1
    @subsection  = 0
    number       = "#{@chapter}.#{@section}"
    "<h3><a name=\"sec-#{number}\"></a>#{section}</h3>\n\n"
  end

  # The subsection:
  def convertSubsection(subsection)
    @subsection += 1
    number       = "#{@chapter}.#{@section}.#{@subsection}"
    "<h4><a name=\"sec-#{number}\"></a>#{subsection}</h4>\n\n"
  end

  # The numeral section:
  def convertNumeralSection(section)
    "<h2 class=\"game\"><a name=\"#{section}\"></a>#{section}</h2>\n\n"
  end

  # The list:
  def convertList(block)
    items = ''
    block.split(/^\*/).each do |item|
      items += "  <li>#{item.chomp}</li>\n" unless item.empty?
    end
    "<ul>\n#{items}</ul>\n\n"
  end

  # The enumeration:
  def convertEnumeration(block)
    items = ''
    block.split(/^#/).each do |item|
      items += "  <li>#{item.chomp}</li>\n" unless item.empty?
    end
    "<ol>\n#{items}</ol>\n\n"
  end

  # The guidepost:
  def convertGuidepost(block)
    items = ''
    block.split(/^;/).each do |item|
      next if item.empty?
      section, text = item.split(/:/, 2); section.strip!; text.strip!
      items += "<div class=\"guidepost\"><span class=\"leader\">&nbsp;" +
               "</span><span class=\"left\">#{text}</span> <a href=\"#" +
               "#{section}\" class=\"right\">#{section}</a></div>\n"
    end
    "#{items}\n\n"
  end

  # The paragraph:
  def convertParagraph(block)
    "<p>#{block}</p>\n\n"
  end

  # The strong opening mark:
  def convertStrongOpen
    '<b>'
  end

  # The strong closing mark:
  def convertStrongClose
    '</b>'
  end

  # The emphasis opening mark:
  def convertEmphasisOpen
    '<i>'
  end

  # The emphasis closing mark:
  def convertEmphasisClose
    '</i>'
  end

  # The numeral link mark:
  def convertNumeralLink(link)
    "<a href=\"##{link}\">#{link}</a>"
  end

  # The link mark:
  def convertLink(link, description)
    if description.empty?
      "<a href=\"#{link}\">#{link.gsub(/^[^:]+:\/{0,2}|\/$/, '')}</a>"
    else
      "<a href=\"#{link}\">#{description}</a>"
    end
  end
end


# Conversion to XHTML, single page:
class GBXhtml < GBHtml
  # The document header:
  def convertHeader
    return <<"END_HEADER"
<?xml version="1.0" encoding="#{@encoding}"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>
  <title>#{@title}</title>
  <style type="text/css">
    body {
      background-color: #FFFFFF;
      color:            #000000;
    }

    h1 {
      text-align:       center;
    }

    h2.author {
      font-size:        x-large;
      font-weight:      normal;
      text-align:       center;
    }

    h2.year {
      font-size:        medium;
      font-weight:      normal;
      text-align:       center;
    }

    h2.game {
      font-size:        large;
      text-align:       center;
    }

    .guidepost {
      position:         relative;
      padding:          0;
      margin:           0;
    }

    .leader {
      position:         absolute;
      border-bottom:    1px dotted black;
      left:             0px;
      top:              -4px;
      width:            100%;
      height: 100%;
    }

    .left {
      background-color: #FFFFFF;
      position:         relative;
      padding-right:    2px;
    }

    .right {
      background-color: #FFFFFF;
      position:         absolute;
      right:            0;
      bottom:           2px;
      padding-left:     2px;
    }
  </style>
  <!--[if IE]>
    <style type="text/css">
      .right { top: 0px; }
    </style>
  <![endif]-->
</head>

<body>
END_HEADER
  end

  # The strong opening mark:
  def convertStrongOpen
    '<strong>'
  end

  # The strong closing mark:
  def convertStrongClose
    '</strong>'
  end

  # The emphasis opening mark:
  def convertEmphasisOpen
    '<em>'
  end

  # The emphasis closing mark:
  def convertEmphasisClose
    '</em>'
  end
end


# Conversion to plain text:
class GBAscii < GBConv
  # The object constructor:
  def initialize(parindent=nil, parskip=nil, leftmargin=nil, itemsep=nil)
    # Paragraph appearance:
    @parindent  = parindent  || 0                # Paragraph indentation.
    @parskip    = parskip    || 1                # Number of empty lines
                                                 # between two paragraphs.
    # List appearance:
    @leftmargin = leftmargin || 2                # List indentation.
    @itemsep    = itemsep    || 0                # Number of empty lines
                                                 # between two items.
    # Section counters:
    @chapter = @section = @subsection = 0

    super()
  end
  
  # Substitute all possibly reserved characters:
  def convertReservedCharacters(block)
    block
  end

  # The document header:
  def convertHeader
    "#{@title.upcase}\n\n#{@author}\n\n#{@year}\n\n"
  end

  # The document footer:
  def convertFooter
    ''
  end

  # The document front matter:
  def convertFrontMatter
    ''
  end

  # The document back matter:
  def convertBackMatter
    ''
  end

  # The document game matter:
  def convertGameMatter
    ''
  end

  # The chapter:
  def convertChapter(chapter)
    @section  = @subsection = 0
    @chapter += 1
    "\n#{@chapter}  #{chapter}\n\n"
  end

  # The section:
  def convertSection(section)
    @subsection = 0
    @section   += 1
    "\n#{@chapter}.#{@section}  #{section}\n\n"
  end

  # The subsection:
  def convertSubsection(subsection)
    @subsection += 1
    "\n#{@chapter}.#{@section}.#{@subsection}  #{subsection}\n\n"
  end

  # The numeral section:
  def convertNumeralSection(section)
    "\n#{section}\n\n"
  end

  # The list:
  def convertList(block)
    block.sub(/^\*\s*/, '').split(/^\*\s*/).collect do |item|
      ' '  *  @leftmargin + '* ' + item.gsub(/\n/, ' ').squeeze(' ') +
      "\n" * (@itemsep + 1)
    end.join + "\n" * @parskip
  end

  # The enumeration:
  def convertEnumeration(block)
    index = 0
    block.sub(/^#\s*/, '').split(/^#\s*/).collect do |item|
      index += 1
      ' '  *  @leftmargin + "#{index}. "+item.gsub(/\n/,' ').squeeze(' ') +
      "\n" * (@itemsep + 1)
    end.join + "\n" * @parskip
  end

  # The guidepost:
  def convertGuidepost(block)
    block.sub(/^;/, '').split(/^;\s*/).collect do |item|
      section, text = item.split(/:/, 2); section.strip!; text.strip!
      text.gsub(/\n/, ' ').squeeze(' ').sub(/^/, ' ' * @parindent) +
      "...[#{section}]" + "\n" *  (@parskip + 1)
    end.join
  end

  # The paragraph:
  def convertParagraph(block)
    block.gsub(/\n/, ' ').squeeze(' ').sub(/^/, ' ' * @parindent) +
    "\n" * (@parskip + 1)
  end

  # The strong opening mark:
  def convertStrongOpen
    ''
  end

  # The strong closing mark:
  def convertStrongClose
    ''
  end

  # The emphasis opening mark:
  def convertEmphasisOpen
    ''
  end

  # The emphasis closing mark:
  def convertEmphasisClose
    ''
  end

  # The numeral link mark:
  def convertNumeralLink(link)
    "[#{link}]"
  end

  # The link mark:
  def convertLink(link, description)
    if description.empty?
      link.gsub(/^[^:]+:\/{0,2}|\/$/, '')
    else
      "#{description} (#{link})"
    end
  end
end


class IO
  # Parse header:
  def get_header
    title, author, year = '', '', Time.now.year.to_s

    while buf = self.getc
      next if buf.chr =~ /[ \t]/
      self.ungetc(buf)
      break if buf == ?=

      case self.gets
        when /^title:\s*(.+)/  then title  = $1
        when /^author:\s*(.+)/ then author = $1
        when /^year:\s*(.+)/   then year   = $1
      end
    end

    return title, author, year
  end

  # Return single block of text:
  def get_block
    result, type, lines = '', nil, nil

    while buf = self.getc
      next if buf.chr =~ /[ \t]/
      self.ungetc(buf)

      if type != nil
        case buf.chr
          when "\n"    then break
          when '='     then break
          when /[*#;]/ then break if type != buf
          else              break if type.chr !~ /[a*#;]/
        end
      else
        case buf.chr
          when "\n"     then self.gets; next
          when /[=*#;]/ then type = buf
          else               type = ?a
        end
        lines = self.lineno + 1
      end

      result += self.gets
    end

    return nil if result.empty?

    lines = "#{lines}-#{self.lineno}" if lines < self.lineno
    return result.chomp, lines
  end
end


class String
  # Substitute HTML entities:
  def to_html
    self.gsub(/\.{3}|&|-{2,3}|<|>|"[^\s]|[^\s]"/) do |match|
      case match
        when '...' then '&hellip;'
        when '&'   then '&amp;'
        when '---' then '&mdash;'
        when '--'  then '&ndash;'
        when '<'   then '&lt;'
        when '>'   then '&gt;'
        when /"([^\s])/ then "&ldquo;#{$1}"
        when /([^\s])"/ then "#{$1}&rdquo;"
        else match
      end
    end
  end

  # Substitute LaTeX reserved characters:
  def to_latex
    self.gsub(/\.{3}|&|_|%|\{|\}|<|>|\$|#|\\|~|\^|"[^\s]|[^\s]"/) do
      |match|
      case match
        when '...' then '\,\dots'
        when '&'   then '\\&'
        when '%'   then '\\%'
        when '_'   then '\\_'
        when '{'   then '\\{'
        when '}'   then '\\}'
        when '<'   then '$<$'
        when '>'   then '$>$'
        when '$'   then '\\$'
        when '#'   then '\\#'
        when '\\'  then '$\\backslash$'
        when '~'   then '$\\sim$'
        when '^'   then '$\\hat{\\null}$'
        when /"([^\s])/ then "\\ldquo{}#{$1}"
        when /([^\s])"/ then "#{$1}\\rdquo{}"
        else match
      end
    end
  end

  # Substitute troff reserved characters:
  def to_troff
    self.gsub(/^'[^']|\.|---|--|\\|"[^\s]|[^\s]"/) do |match|
      case match
        when '.'   then '\\.'
        when '---' then '\\[em]'
        when '--'  then '\\[en]'
        when '\\'  then '\\(rs'
        when /'([^'])/  then "\\[cq]#{$1}"
        when /"([^\s])/ then "\\[lq]#{$1}"
        when /([^\s])"/ then "#{$1}\\[rq]"
        else match
      end
    end
  end
end


if __FILE__ == $0
  require 'getoptlong'

  # Display given error to the standard error output and exit:
  def exitWithError(message, value=Errno::EPERM::Errno)
    STDERR.puts "#{File.basename($0, '.rb')}: #{message}."
    exit value
  end

  # Display script usage:
  def displayHelp
    name = File.basename($0, '.rb')
    STDOUT.puts <<"END_HELP"
Usage: #{name} -t html  [-e enc] [-o file] [file]
       #{name} -t xhtml [-e enc] [-o file] [file]
       #{name} -t latex [-a|-b] [-n name] [-e enc] [-p pkgs] [-o file] [file]
       #{name} -t troff [-a|-b] [-n name] [-o file] [file]
       #{name} -t ascii [-w width] [-l lines] [-W width] [-L lines]
                       [-o file] [file]

General options:

  -t, --type type          specify target document type;  available options
                           are: html, xhtml, latex, troff and ascii
  -o, --output file        specify the output file name
  -h, --help               display this help and exit
  -v, --version            display version information and exit

html and xhtml related options:

  -e, --encoding encoding  specify the source file encoding

latex related options:

  -a, --article            produce the document class `article'
  -b, --book               produce the document class `book'
  -e, --encoding encoding  specify the source file encoding
  -n, --toc-name heading   specify the table of contents heading
  -p, --packages packages  specify additional LaTeX packages

troff related options:

  -a, --article            produce the document without RP macro
  -b, --book               produce the document using RP macro
  -n, --toc-name heading   specify table of contents heading

ascii related options:

  -w, --par-indent width   specify paragraph indentation
  -l, --par-skip lines     specify number of empty lines between paragraphs
  -W, --item-indent width  specify list indentation
  -L, --item-skip lines    specify number of empty lines between list items
END_HELP
  end

  # Display script version:
  def displayVersion
    STDOUT.puts <<"END_VERSION"
#{GBConv::NAME}, #{GBConv::VERSION}

Copyright (C) 2007, 2008 Jaromir Hradilek
This program is free software; see the source for copying conditions. It is
distributed in the hope  that it will be useful,  but WITHOUT ANY WARRANTY;
without even the implied warranty of  MERCHANTABILITY or FITNESS FOR A PAR-
TICULAR PURPOSE.
END_VERSION
  end

  # Command-line options:
  opts = GetoptLong.new(
    [ '--type',        '-t', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--output',      '-o', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--help',        '-h', GetoptLong::NO_ARGUMENT ],
    [ '--version',     '-v', GetoptLong::NO_ARGUMENT ],
    [ '--article',     '-a', GetoptLong::NO_ARGUMENT ],
    [ '--book',        '-b', GetoptLong::NO_ARGUMENT ],
    [ '--encoding',    '-e', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--toc-name',    '-n', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--packages',    '-p', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--par-indent',  '-w', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--par-skip',    '-l', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--item-indent', '-W', GetoptLong::REQUIRED_ARGUMENT ],
    [ '--item-skip',   '-L', GetoptLong::REQUIRED_ARGUMENT ]
  )

  # Supported target document types and their extensions:
  supported  = {
    'html'   => '.html',
    'xhtml'  => '.html',
    'latex'  => '.tex',
    'troff'  => '.ms',
    'ascii'  => '.txt',
  }

  # Default options:
  outfile    = '-'                               # Output destination.
  type       = 'html'                            # Target document type.
  article    = false                             # Target document subtype.
  encoding   = nil                               # Input encoding.
  tocname    = nil                               # Table of contents name.
  packages   = nil                               # List of LaTeX packages.
  parindent  = nil                               # Paragraph indentation.
  parskip    = nil                               # Paragraph spacing.
  leftmargin = nil                               # List indentation.
  itemsep    = nil                               # List spacing.

  # Begin protected section:
  begin
    # Set the command-line options parser:
    opts.quiet = true

    # Process command-line options:
    opts.each do |opt, arg|
      case opt
        when '--help'        then displayHelp;    exit 0
        when '--version'     then displayVersion; exit 0
        when '--article'     then article    = true;
        when '--book'        then article    = false;
        when '--type'        then type       = arg
        when '--output'      then outfile    = arg
        when '--encoding'    then encoding   = arg
        when '--toc-name'    then tocname    = arg
        when '--packages'    then packages   = arg
        when '--par-indent'  then parindent  = arg.to_i
        when '--par-skip'    then parskip    = arg.to_i
        when '--item-indent' then leftmargin = arg.to_i
        when '--item-skip'   then itemsep    = arg.to_i
      end
    end

    # Make sure the output type is valid:
    unless supported.has_key?(type)
      raise GetoptLong::Error, "Wrong document type `#{type}'"
    end

    # Input and output settings:
    infile = ARGV[0]  || '-'
    input  = (infile  == '-') ? STDIN  : File.open(infile,  'r')
    output = (outfile == '-') ? STDOUT : File.open(outfile, 'w')

    # Prepare the conversion:
    case type
      when 'html'
        gb = GBHtml.new(encoding)
      when 'xhtml'
        gb = GBXhtml.new(encoding)
      when 'latex'
        gb = article ? GBLatex.new(encoding, packages) \
                     : GBBook.new(encoding,  packages, tocname)
      when 'troff'
        gb = article ? GBTroff.new \
                     : GBReport.new(tocname)
      when 'ascii'
        gb = GBAscii.new(parindent, parskip, leftmargin, itemsep)
    end

    # Proceed with conversion:
    gb.convert(input, output)

    # Close opened files if any:
    input.close  if ARGV[0]
    output.close if outfile

  # Handle possible exceptions:
  rescue GetoptLong::Error
    exitWithError($!, Errno::EINVAL::Errno)
  rescue Errno::ENOENT
    exitWithError($!, Errno::ENOENT::Errno)
  rescue Errno::EACCES
    exitWithError($!, Errno::EACCES::Errno)
  rescue
    exitWithError($!, Errno::EPERM::Errno)
  end

  # Return success:
  exit 0
end
